#
# -*- coding: utf-8 -*-
#
import datetime
import time
import string
import re
import urllib
import types

from base64 import b64encode
from xml.etree import ElementTree

from django.http import *
from django.shortcuts import render_to_response
from django.template import loader
from django.utils import simplejson
from django.core import serializers

from google.appengine.api import users
from google.appengine.api import urlfetch
from google.appengine.ext.db import GqlQuery
from google.appengine.ext import db

import logging

models = {}
model_home_views = {}
modelviews = {}
entityviews = {}
pre_model_views = {}
post_model_views = {}
pre_entity_views = {}
post_entity_views = {}
pre_saves = {}
post_saves = {}
pre_deletes = {}
post_deletes = {}
  
def _model_decorator(map, name, type=None, function=None):
  if not function:
    def dec(func):
      return _model_decorator(map, name, type, func)
    return dec
  _register(map, name, type, function)
  return function
  
def _register(map, name, type, function):
  if name not in map:
    map[name] = {}
  if type:
    map[name][type] = function
  else:
    map[name][name] = function

def _get_function(map, name, type=None):
  if name not in map:
    return None
  if type and type in map[name]:
    return map[name][type]
  elif name in map[name]:
    return map[name][name]
  return None
  
# decorators
def model(model, name):
  models[name] = model
  def dec(func):
    return func
  return dec

def model_home(model, function=None):
  if not function:
    def dec(func):
      return model_home(model, func)
    return dec
  model_home_views[model] = function
  return function

def model_view(model, view=None, function=None):
  return _model_decorator(modelviews, model, view, function)
  
def entity_view(model, view=None, function=None):
  return _model_decorator(entityviews, model, view, function)
  
def pre_model_view(model, view=None, function=None):
  return _model_decorator(pre_model_views, model, view, function)
  
def post_model_view(model, view=None, function=None):
  return _model_decorator(post_model_views, model, view, function)
  
def pre_entity_view(model, view=None, function=None):
  return _model_decorator(pre_entity_views, model, view, function)
  
def post_entity_view(model, view=None, function=None):
  return _model_decorator(post_entity_views, model, view, function)

def pre_save(model):
  return _model_decorator(pre_saves, model, None, function)
  
def post_save(model):
  return _model_decorator(post_saves, model, None, function)
  
def pre_delete(model):
  return _model_decorator(pre_deletes, model, None, function)
  
def post_delete(model):
  return _model_decorator(post_deletes, model, None, function)
  
def transaction(function):
  def decorate(*args, **kwds):
    return db.run_in_transaction(function, *args, **kwds)
  return decorate

#service

re_model = re.compile(r'(?P<model>.[^\/\.]*)/')
re_model_view = re.compile(r'(?P<model>.[^\/]*?)/(?P<view>.[^\/\.]*)/')
re_model_details = re.compile(r'(?P<model>.[^\/]*?)/(?P<key>\d*)/')
re_model_details_view = re.compile(r'(?P<model>.[^\/]*?)/(?P<key>\d*?)/(?P<view>.[^\/\.]*)/')
re_file_ext = re.compile(r'.[^\.]*\.(?P<ext>.*)')

def service(request, path, ext_params=None, context=None):
    if not ext_params:
        ext_params = {}
    if not context:
        context = {}
    #parse path
    mo = re_model_details_view.match(path) or re_model_details.match(path)\
             or re_model_view.match(path) or re_model.match(path)
             
    if not mo:
        raise Http404()
    
    model_name, key, view = [None, None, None]
    if 'model' in mo.groupdict():
        model_name = mo.groupdict()['model']
    if 'key' in mo.groupdict():
        key = mo.groupdict()['key']
    if 'view' in mo.groupdict():
        view = mo.groupdict()['view']
    mo = re_file_ext.search(path)
    if mo:
        ext = mo.groups()[0]
    else:
        ext = ''
        
    if model_name in models:
        model = models[model_name]
    else:
        raise Http404()
        
    #process result
    result = {}
    method = request.method
    params = request.method == 'GET' and request.GET.copy() or request.POST.copy()
    params.update(ext_params)
    
    if key:
        _process_pre_entity_view(request, model, view, params, context)
        entity = model.get_by_id(int(key))
        if not entity:
            raise Http404()
        result = _entity_view(request, model, entity, view, params, context)
        _process_post_entity_view(request, model, entity, view, params, context)
    else:
        _process_pre_model_view(request, model, view, params, context)
        result = _model_view(request, model, view, params, context)
        _process_post_model_view(request, model, view, params, context)
        
    if isinstance(result, HttpResponse):
        return result
    
    #render json, xml
    if ext=='json':
        r = HttpResponse(simplejson.dumps(result))
        r.headers['Content-Type'] = 'application/json; charset=utf-8'
        return r
    if ext=='xml':
        return HttpResponse(simplejson.dumps(result))
    
    if not result:
        result = {}
    
    #render html
    if 'template' in result:
        return render_to_response(result['template'], result)
    if 'redirect' in result:
        return HttpResponseRedirect(result['redirect'])
    
    templates = []                
    if view=='delete':
        if request.method == 'POST':
            return HttpResponseRedirect('../../')
        else:
            templates.append('%s-delete%s'%(model_name, ext and '.'+ext or '.html'))
            templates.append('rest/model-delete%s'%(ext and '.'+ext or '.html'))
    elif key or view=='add':
        templates.append('%s/model-%s%s'%(model_name, view and view or 'update', ext and '.'+ext or '.html'))
        templates.append('rest/model%s%s'%(view and '-'+view or '-update', ext and '.'+ext or '.html'))
        templates.append('rest/model%s'%(ext and '.'+ext or '.html'))
    else:
        templates.append('%s/model-%s%s'%(model_name, view and view or 'list', ext and '.'+ext or '.html'))
        templates.append('%s/model-list%s'%(model_name, ext and '.'+ext or '.html'))
        templates.append('rest/model-list%s'%(ext and '.'+ext or '.html'))
    return render_to_response(templates, result)


re_filter_param = re.compile(r'f-(?P<name>.*?)-(?P<operator>.*)')
re_order_param = re.compile(r'o-(?P<name>.*)')
re_page = re.compile(r'(?<=page\=)(?P<page>\d*)')
    
#filter, type convert
#order
    
def _model_view(request, model, view, params, context):
    result = {}
    if not view and model in model_home_views:
        f = model_home_views[model]
        result = f(request, model, params, context)
        if result:
            return result
    if view:
        if model in modelviews and view in modelviews[model]:
            f = _get_function(modelviews, model, view)
            if f:
                result = f(request, model, view, params, context)
            if result:
                return result
        if view == 'add':
            return rest_model_details(request, model, None, params, context)
    f = _get_function(modelviews, model, view)
    if f:
        result = f(request, model, view, params, context)
    if result:
        return result
    return rest_model_list(request, model, params, context)

def _process_pre_model_view(request, model, view, params, context):
    f = _get_function(pre_model_views, model, view) or _get_function(pre_model_views, model, None)
    if f:
        result = f(request, model, view, params, context)

def _process_post_model_view(request, model, view, params, context):
    f = _get_function(post_model_views, model, view) or _get_function(post_model_views, model, None)
    if f:
        result = f(request, model, view, params, context)

def _entity_view(request, model, entity, view, params, context):
    if view == 'delete':
        return rest_model_delete(request, model, entity)
    f = _get_function(entityviews, model, view) or _get_function(entityviews, model, None)
    if f:
        result = f(request, model, entity, view, params, context)
        if result:
            return result
    return rest_model_details(request, model, entity, params, context)

def _process_pre_entity_view(request, model, view, params, context):
    f = _get_function(pre_entity_views, model, view) or _get_function(pre_entity_views, model, None)
    if f:
        result = f(request, model, view, params, context)

def _process_post_entity_view(request, model, entity, view, params, context):
    f = _get_function(post_entity_views, model, view) or _get_function(post_entity_views, model, None)
    if f:
        result = f(request, model, entity, view, params, context)


def rest_model_details(request, model, entity, params, ext_context):
    if not ext_context:
        ext_context = {}
    if request.method == 'POST':
        if not('errors' in ext_context and ext_context['errors']):
            if not entity:
                entity = model()
            for k, v in params.items():
                try:
                    setattr(entity, k, _from_string(model, k, v, request))
                except Exception:
                    pass
            entity.put()
            return {'redirect': '../../%s/%s/' % (model.Metadata.name, entity.key().id()), 'entity':entity}
    context = {'entity':entity, 'model':model}
    context.update(ext_context)
    return context

def rest_model_delete(request, model, entity):
    if entity and request.method == 'POST':
        entity.delete()
    return {'entity':entity, 'model':model}

def rest_model_list(request, model, params, ext_context):
    conditions = []
    orders = []
    param_num = 1
    for k, v in params.items():
        mo = re_filter_param.match(k)
        if mo:
            name, operator = mo.groups()
            conditions.append(('%s %s :%s' %(name, _get_operator(operator), param_num), _from_string(model, name, v)))
            param_num += 1
        else:
            mo = re_order_param.match(k)
            if mo:
                orders.append('%s %s'%(mo.groups()[0], v))
            
    condition = ' and '.join([k for k, v in conditions])
    page = 'page' in params and int(params['page']) or 1
    
    pagesize = 'pagesize' in params and int(params['pagesize']) or hasattr(model.Metadata, 'pagesize') and model.Metadata.pagesize or 20
    if condition:
        gql = 'select * from %s where %s' % (model.__name__, condition)
    else:
        gql = 'select * from %s' % model.__name__
    if orders:
        gql += ' order by %s'%', '.join(orders)
    logging.info('REST - GQL: %s' % gql)
    logging.info('REST - Parameters: %s' % [v for k, v in conditions])
    if conditions:
        q = db.GqlQuery(gql, *[v for k, v in conditions])
    else:
        q = db.GqlQuery(gql)
    results = q.fetch(pagesize+1, pagesize*(page-1))
    params['page'] = page
    path = u'%s?%s'%(_to_unicode(request.path), u'&'.join(u'%s=%s'%(k,isinstance(v, str) and v.decode('utf-8') or v) for k,v in params.items()))
    pre_page = page>1 and re.sub('(?<=page\=)(\d*)', str(page-1), path) or None
    first_page = page>2 and re.sub('(?<=page\=)(\d*)', '1', path) or None
    next_page = len(results)>pagesize and re.sub('(?<=page\=)(\d*)', str(page+1), path) or None
    results = next_page and results[0:pagesize] or results
    current_path = request.path
    context = {'results':results, 'page':page, 'pagesize':pagesize, 
            'first_page':first_page, 'pre_page':pre_page, 'next_page':next_page, 'current_path':current_path, 'model':model}
    context.update(ext_context)
    return context

def _to_list_dict(model, entity):
    dict = {'id':entity.key().id_or_name()}
    for p,l in model.Metadata.list:
        dict[p] = _to_string(model, p, getattr(entity, p))
    return dict

def _from_string(model, property, value, request=None):
    if type(value) == str or type(value) == unicode:
        t = getattr(model, property).__class__
        if t == db.StringProperty:
            return _to_unicode(value)
        if t == db.TextProperty:
            return _to_unicode(value)
        if t == db.IntegerProperty:
            return int(value)
        if t == db.StringListProperty:
            if ',' in value:
                return [v.strip() for v in _to_unicode(value).split(',')]
            if request:
                return [_to_unicode(v) for v in request.REQUEST.getlist(property)]
            return _to_unicode(value)
        if t == db.BooleanProperty:
            return (len(value)>0 and string.lower(value[0])=='t') or value == 'on'
        if t == db.FloatProperty:
            return float(value)
    return value

def _to_unicode(str):
    if isinstance(str, unicode):
        return str
    return unicode(str, 'utf-8') 

def _to_string(model, property, value):
    if callable(value):
        return value()
    clazz = getattr(model, property).__class__
    if clazz == db.StringListProperty:
        return ', '.join(value)
    return unicode(value)

def _get_operator(name):
    if name=='eq':
        return '='
    if name=='lt':
        return '<'
    if name=='gt':
        return '>'